/*
 * File Name: proxy.c
 *
 * Group Members: Cassie Chin (cassiechin9793@gmail.com)
 *                Stephen Smetana (stephensmetana11@gmail.com)
 *                Logan Widick (logan.widick@gmail.com)
 *
 * Sources: http://beej.us/guide/bgnet/examples/server.c
 *          This code is originally intended to send the string "Hello, World!\n"
 *          out over a stream connection.
 *
 *          We took the outline of this code and incorporated the ability to
 *          perform actions in a child process. This child process recieves
 *          HTTP requests from a client.
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdbool.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <stddef.h>
#include <sys/stat.h>
#include <assert.h>
#include "httpstuff.h"
#include "filter.h"
#include "blacklist.h"
#include <fcntl.h>  

/* We can delete everything we don't need here before we turn in the project 
 Turn statement to ON to use any of these features */
#define DEBUG_OFF
#define SETSOCKOPT_OFF
#define REAP_DEAD_PROCESSES_OFF

#define PORT "60003"  // the port users will be connecting to
#define BACKLOG 10     // how many pending connections queue will hold
#define BUFFERSIZE 256

#define PROXY_NAME "http://localhost:60003/"

void main_child(int fd);

void sigchld_handler(int s) {
	while (waitpid(-1, NULL, WNOHANG) > 0)
		;
}

// get sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa) {
	if (sa->sa_family == AF_INET) {
		return &(((struct sockaddr_in*) sa)->sin_addr);
	}

	return &(((struct sockaddr_in6*) sa)->sin6_addr);
}

int main(void) {
	int sockfd, new_fd; // listen on sock_fd, new connection on new_fd
	struct addrinfo hints, *servinfo, *p;
	struct sockaddr_storage their_addr; // connector's address information
	socklen_t sin_size;
	struct sigaction sa;
	char s[INET6_ADDRSTRLEN];
	int rv;

	memset(&hints, 0, sizeof hints);
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_flags = AI_PASSIVE; // use my IP

	if ((rv = getaddrinfo(NULL, PORT, &hints, &servinfo)) != 0) {
		fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
		freeaddrinfo(servinfo);
		return 1;
	}

	// loop through all the results and bind to the first we can
	for (p = servinfo; p != NULL; p = p->ai_next) {
		if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol))
				== -1) {
			perror("server: socket");
			continue;
		}

#ifdef SETSOCKOPT_ON
		if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes,
						sizeof(int)) == -1) {
			perror("setsockopt");
			exit(1);
		}
#endif

		if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
			close(sockfd);
			perror("server: bind");
			continue;
		}

		break;
	}

	if (p == NULL) {
		fprintf(stderr, "server: failed to bind\n");
		return 2;
	}

	freeaddrinfo(servinfo); // all done with this structure

	if (listen(sockfd, BACKLOG) == -1) {
		perror("listen");
		exit(1);
	}

#ifdef REAP_DEAD_PROCESSES_ON
	sa.sa_handler = sigchld_handler; // reap all dead processes
	sigemptyset(&sa.sa_mask);
	sa.sa_flags = SA_RESTART;
	if (sigaction(SIGCHLD, &sa, NULL) == -1) {
		perror("sigaction");
		exit(1);
	}
#endif

	printf("server: waiting for connections...\n");

	while (1) { // main accept() loop
		sin_size = sizeof their_addr;
		new_fd = accept(sockfd, (struct sockaddr *) &their_addr, &sin_size);
		if (new_fd == -1) {
			perror("accept");
			continue;
		}

		inet_ntop(their_addr.ss_family,
				get_in_addr((struct sockaddr *) &their_addr), s, sizeof s);
		printf("server: got connection from %s\n", s);

		/* BEGIN CHILD PROCESS : HTTP request processing is done here*/
		if (!fork()) {
			close(sockfd); // child doesn't need the listener
			main_child(new_fd);
			close(new_fd);
			exit(0);
		}
		/* END CHILD PROCESS */
		close(new_fd); // parent doesn't need this
	}

	return 0;
}

void sendError(int sock_in_fd, int errorCode) {
	HTTPMsg *proxy_resp = new_HTTPMsg();
	proxy_resp->version = 1.0;
	proxy_resp->statusCode = errorCode;

	//make the body
	proxy_resp->fileptr = tmpfile();
	char tmp_body_buffer[BUFFER_SIZE + 1];
	snprintf(tmp_body_buffer, BUFFER_SIZE, "<html><h1>%d Error</h1></html>\n",
			errorCode);
	fwrite(tmp_body_buffer, sizeof(char), strlen(tmp_body_buffer),
			proxy_resp->fileptr);
	fflush(proxy_resp->fileptr);
	fseek(proxy_resp->fileptr, 0, SEEK_END);
	proxy_resp->contentLength = (int) ftell(proxy_resp->fileptr);

	// send over sock_out_fd
	HTTPMsg_sendMsg(proxy_resp, sock_in_fd);
	delete_HTTPMsg(proxy_resp);
}

/**
 * Convenience method for opening a file on the system. 
 * @param path the path to the file
 * @param iswrite if true, use write access. Otherwise, use read access.
 * @return a new FILE* or null.
 */
FILE* open_file(char *path, bool isWrite) {
	if (!path)
		return NULL;
	else if (!isWrite)
		return fopen(path, "r");
	else {
		//find first slash.
		int i = strlen(path) - 1;
		for (; i >= 0; i--) {
			if (path[i] == '/')
				break;
		}

		char *substring = calloc(i + 1, sizeof(char));
		strncpy(substring, path, i);

		//make the intermediate directories.
		char *syscall_cmd = calloc(strlen("mkdir -p ") + strlen(substring) + 3,
				sizeof(char));
		sprintf(syscall_cmd, "mkdir -p %s", substring);
		printf("System call to make directory returned %d\n",
				system(syscall_cmd));
		free(syscall_cmd);
		free(substring);

		return fopen(path, "w");
	}
}

/**
 * This is the function the proxy starts to communicate between a client and server.
 * @param new_fd the fd of the socket.
 */
void main_child(int new_fd) {
	//    struct sockaddr_in client_address;
	HTTPMsg *recv_msg = NULL;
	HTTPMsg *proxy_resp = NULL;
	HTTPMsg *server_resp = NULL;

	int proxy_to_server_fd = 0;
	int server_to_proxy_fd = 0;
	char *filePath = NULL;

	char xfer_buffer[BUFFER_SIZE + 1];
	int num_xfer = 0;

	int parse_code = 0;

	//we use to try to use nonblocking fds but this failed.
	//set file to nonblocking. 
	//int flags = fcntl(new_fd, F_GETFL, 0); 
	//flags |= O_NONBLOCK; 
	//fcntl(new_fd, F_SETFL, flags);

	/*
	 * previously, these were separate FILE pointers
	 * created with fdopen and dupfd. 
	 * Now, they are the same file descriptor. 
	 * The old naming system has been retained for clarity of direction.
	 * However, as they are now the same socket you only need to close one. 
	 */
	int client_to_proxy_fd = new_fd;
	int proxy_to_client_fd = new_fd;

	// Open, Create and Close the Blacklist file
	FILE * blacklist_input = fopen("blacklist.txt", "r");
	if (blacklist_input == NULL)
		printf(" Error! blacklist.txt not found! \n");
	Blacklisted *blacklist = new_Blacklisted(blacklist_input);
	fclose(blacklist_input);

	// Open, Create and Close the Badwords file        
	FILE *badWordsFile = fopen("badwords.txt", "r");
	Filter *filter = new_Filter(badWordsFile);
	if (filter == NULL)
		printf("Error! badwords.txt not found! \n");
	fclose(badWordsFile);

	printBlacklist(blacklist);
	//The steps repeat for additional messages
	while (1) {

		// #2 -- complete
		fprintf(stderr, "Parsing client request.\n");
		fflush(stderr);
		recv_msg = new_HTTPMsg();
		recv_msg->isRequest = true;
		//// changed this so new_fd is now passed into it. 
		parse_code = HTTPMsg_doParsing(recv_msg, client_to_proxy_fd, false);
		if (parse_code != 0) {
			fprintf(stderr, "doParsing on client request returned %d\n",
					parse_code);
			delete_HTTPMsg(recv_msg);
			sendError(proxy_to_client_fd, 400);
			recv_msg = NULL;
			break;
		}

		//remove the accept-encoding header. we do not allow gzip, deflate, etc. 
		HeaderList_removeHeaderByKey(recv_msg->headerList, "Accept-Encoding");
		HeaderList_removeHeaderByKey(recv_msg->headerList, "Connection");
		HeaderList_addHeader(recv_msg->headerList, "Connection", "close");
		HeaderList_removeHeaderByKey(recv_msg->headerList, "Bytes");
		HeaderList_removeHeaderByKey(recv_msg->headerList, "Range");
		//recv_msg->version = 1.0f;
		HTTPMsg_print(recv_msg, stderr);
		fprintf(stderr, "Parsed client request.\n");
		fflush(stderr);

		//we should have reached the BODY, TRAILERS, or END state.
		if (recv_msg->state != BODY && recv_msg->state != END && recv_msg->state != TRAILERS) {
			fprintf(stderr, "Invalid ending state. Exiting.\n");

			sendError(proxy_to_client_fd, 400);
			delete_HTTPMsg(recv_msg);
			recv_msg = NULL;
			break;
		}

		//Do the url blacklist detection here. BREAK if the url is blacklisted.
		if (isThisBlacklisted(blacklist, recv_msg->host)) {
			printf("YOU SHALL NOT LOAD\n%s is Blacklisted!\n", recv_msg->host);

			sendError(proxy_to_client_fd, 403);
			delete_HTTPMsg(recv_msg);
			recv_msg = NULL;
			break;
		}

		filePath = HTTPMsg_findFilePath(recv_msg, false);

		//no file path; send error
		if (filePath == NULL && !HTTPMsg_isDynamic(recv_msg, false)) {
			fprintf(stderr, "Cannot make a file path.\n");

			sendError(proxy_to_client_fd, 500);
			delete_HTTPMsg(recv_msg);
			recv_msg = NULL;
			break;
		}
		//file path exists. let's try to find the file.
		else {
			FILE *file_to_send = open_file(filePath, false);

			//send from cache
			if (file_to_send && !HTTPMsg_isDynamic(recv_msg, false)) {
				fprintf(stderr, "Sending data from cache for resource %s\n",
						recv_msg->resource);
				//make message; set headers, version, status
				proxy_resp = new_HTTPMsg();
				proxy_resp->isRequest = false;
				proxy_resp->version = 1.0;
				proxy_resp->statusCode = 200;
				if (!proxy_resp->path)
					proxy_resp->path = strdup(recv_msg->path);
				if (!proxy_resp->host)
					proxy_resp->host = strdup(recv_msg->host);

				//HTTPMsg_prependStringToResource(proxy_resp, PROXY_NAME);

				fseek(file_to_send, 0, SEEK_END);

				//filtering does not impact content length, as each char in a bad word gets replaced with a *
				// (at least, this is true under ASCII). 
				proxy_resp->contentLength = (int) ftell(file_to_send);

				//head: headers only. No filtering required, and filtering does not impact content length.
				if (strcasecmp(recv_msg->method, "HEAD") == 0) {
					HTTPMsg_sendMsg(proxy_resp, proxy_to_client_fd);
					fclose(file_to_send);
				}

				//just send it already!
				else {
					proxy_resp->fileptr = file_to_send;
					HTTPMsg_sendMsg(proxy_resp, proxy_to_client_fd);
				}
				HTTPMsg_print(proxy_resp, stderr);
				fprintf(stderr, "Sent data from cache for resource %s\n",
						recv_msg->resource);
				delete_HTTPMsg(proxy_resp);
				proxy_resp = NULL;
			}

			//do not send from cache. request from server.
			else {
				//have we opened a connection?
				if (proxy_to_server_fd == 0 || server_to_proxy_fd == 0) {
					FilePointerSet mySet;
					fprintf(stderr,
							"Trying to open conn to host %s and port %s\n",
							recv_msg->host, recv_msg->port);
					//did we open?
					if (openClientConnection(recv_msg->port, recv_msg->host,
							&mySet) != 0) {
						fprintf(stderr, "UNABLE TO OPEN CONNECTION!\n");
						sendError(proxy_to_client_fd, 404);
						delete_HTTPMsg(recv_msg);
						recv_msg = NULL;
						break;
					}
					//if we did, set the fds. 
					else {
						proxy_to_server_fd = mySet.outFile;
						server_to_proxy_fd = mySet.inFile;
					}
				}

				//we should have a connection. Send the request.
				fprintf(stderr, "About to send request for resource %s\n",
						recv_msg->resource);

				if (HTTPMsg_sendMsg(recv_msg, proxy_to_server_fd) != 0) {
					//connection impossible; return 503
					fprintf(stderr, "UNABLE TO SEND RECV_MSG TO SERVER\n");
					sendError(proxy_to_client_fd, 500);
					delete_HTTPMsg(recv_msg);
					recv_msg = NULL;
					break;
				}
				fprintf(stderr, "Sent the request for resource %s\n",
						recv_msg->resource);

				//now let's get the response.
				fprintf(stderr, "Parsing the server's reply for resource %s\n",
						recv_msg->resource);

				server_resp = new_HTTPMsg();
				server_resp->isRequest = false;
				parse_code = HTTPMsg_doParsing(server_resp, server_to_proxy_fd,
						false);
				if (!server_resp->resource)
					server_resp->resource = strdup(recv_msg->resource);
				if (parse_code != 0) {
					fprintf(stderr,
							"HTTPMsg_doParsing for server returned %d\n",
							parse_code);
					delete_HTTPMsg(recv_msg);
					recv_msg = NULL;
					break;
				}
				HTTPMsg_print(server_resp, stderr);
				fprintf(stderr,
						"Done parsing the server's reply for resource %s\n",
						recv_msg->resource);

				//check validity of response
				if (server_resp->state != BODY && server_resp->state != TRAILERS && server_resp->state != END) {
					fprintf(stderr, "SERVER response has invalid state.\n");
					sendError(proxy_to_client_fd, 404);
					delete_HTTPMsg(recv_msg);
					recv_msg = NULL;
					break;
				}

				fprintf(stderr, "Begin filterchk resource %s\n",
						recv_msg->resource);
				//filter here.
				if (HTTPMsg_shouldFilter(server_resp)) {
					fprintf(stderr, "Beginning filtering resource %s\n",
							recv_msg->resource);
					FILE *filtered = tmpfile();
					Filter_filterFile(filter, server_resp->fileptr, filtered);
					fclose(server_resp->fileptr);
					server_resp->fileptr = filtered;
					fprintf(stderr, "End filtering resource %s\n",
							recv_msg->resource);
				}
				fprintf(stderr, "end filterchk resource %s\n",
						recv_msg->resource);

				//if we have a 2xx and a file pointer, save if not dynamic
				if (server_resp->statusCode >= 200
						&& server_resp->statusCode < 300 && server_resp->fileptr
						&& !HTTPMsg_isDynamic(recv_msg, false)
						&& !HTTPMsg_isDynamic(server_resp, false)) {

					fprintf(stderr, "Begin saving resource %s\n",
							recv_msg->resource);
					char *newFilePath = HTTPMsg_findFilePath(server_resp,
							false);
					if (newFilePath) {
						free(filePath);
						filePath = NULL;
					} else {
						newFilePath = filePath;
						filePath = NULL; //prevents double free.
					}

					//try to save the file
					FILE* local_save_file = open_file(newFilePath, true);
					if (local_save_file) {
						fprintf(stderr, "Saving a local copy\n");
						rewind(server_resp->fileptr);
						while (1) {
							num_xfer = fread(xfer_buffer, sizeof(char),
									BUFFER_SIZE, server_resp->fileptr);

							if (num_xfer <= 0)
								break;
							fwrite(xfer_buffer, sizeof(char), num_xfer,
									local_save_file);
							fflush(local_save_file);
						}
						fclose(local_save_file);
						fprintf(stderr, "Done saving a local copy\n");
					}

					//now free new file path
					if (newFilePath)
						free(newFilePath);
					newFilePath = NULL;
					fprintf(stderr, "End saving resource %s\n",
							recv_msg->resource);
				}

				fprintf(stderr,
						"Sending server's reply to client for resource %s\n",
						recv_msg->resource);

				//we are actually sending content; modify content-length header if needed. 
				HTTPMsg_sendMsg(server_resp, proxy_to_client_fd);
				fprintf(stderr,
						"Done sending server's reply to client  for resource %s\n",
						recv_msg->resource);

				if (server_to_proxy_fd)
					close(server_to_proxy_fd);
				//if(proxy_to_server_fd) close(proxy_to_server_fd);
				proxy_to_server_fd = 0;
				server_to_proxy_fd = 0;

				delete_HTTPMsg(server_resp);
				server_resp = NULL;
				delete_HTTPMsg(recv_msg);
				recv_msg = NULL;
			}
		}
		if (filePath)
			free(filePath);
		filePath = NULL;
		if (proxy_resp)
			delete_HTTPMsg(proxy_resp);
		proxy_resp = NULL;
		if (recv_msg)
			delete_HTTPMsg(recv_msg);
		recv_msg = NULL;
		if (server_resp)
			delete_HTTPMsg(server_resp);
		server_resp = NULL;
	}
	//cleanup
	if (filePath)
		free(filePath);
	if (proxy_to_server_fd)
		close(proxy_to_server_fd);
	//if (server_to_proxy_fd)
	//	close(server_to_proxy_fd);
	if (proxy_to_client_fd)
		close(proxy_to_client_fd);
	//if (client_to_proxy_fd)
	//	close(client_to_proxy_fd);
	if (proxy_resp)
		delete_HTTPMsg(proxy_resp);
	proxy_resp = NULL;
	if (recv_msg)
		delete_HTTPMsg(recv_msg);
	recv_msg = NULL;
	if (server_resp)
		delete_HTTPMsg(server_resp);
	server_resp = NULL;

	//delete the blacklist
	delete_Blacklisted(blacklist);

	//delete the bad words filter
	delete_Filter(filter);
}
